/**
 * Copyright (C) 2009 - 2012 SC 4ViewSoft SRL
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.achartengine.chart;

import java.io.Serializable;
import java.util.List;

import org.achartengine.model.Point;
import org.achartengine.model.SeriesSelection;
import org.achartengine.renderer.DefaultRenderer;
import org.achartengine.renderer.SimpleSeriesRenderer;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYMultipleSeriesRenderer.Orientation;
import org.achartengine.util.MathHelper;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;

/**
 * An abstract class to be implemented by the chart rendering classes.
 */
public abstract class AbstractChart implements Serializable
{
	/**
	 * pieChartColors
	 */
	private int[][] mPieChartColors = null;
	
	/**
	 * The graphical representation of the chart.
	 * 
	 * @param canvas the canvas to paint to
	 * @param x the top left x value of the view to draw to
	 * @param y the top left y value of the view to draw to
	 * @param width the width of the view to draw to
	 * @param height the height of the view to draw to
	 * @param paint the paint
	 */
	public abstract void draw(Canvas canvas, int x, int y, int width, int height, Paint paint);
	
	/**
	 * Draws the chart background.
	 * 
	 * @param renderer the chart renderer
	 * @param canvas the canvas to paint to
	 * @param x the top left x value of the view to draw to
	 * @param y the top left y value of the view to draw to
	 * @param width the width of the view to draw to
	 * @param height the height of the view to draw to
	 * @param paint the paint used for drawing
	 * @param newColor if a new color is to be used
	 * @param color the color to be used
	 */
	protected void drawBackground(DefaultRenderer renderer, Canvas canvas, int x, int y, int width,
			int height, Paint paint, boolean newColor, int color)
	{
		if (renderer.isApplyBackgroundColor() || newColor)
		{
			if (newColor)
			{
				paint.setColor(color);
			}
			else
			{
				paint.setColor(renderer.getBackgroundColor());
			}
			paint.setStyle(Style.FILL);
			canvas.drawRect(x, y, x + width, y + height, paint);
		}
	}
	
	/**
	 * Draws the pieChart Legend.
	 * 
	 * @param canvas the canvas to paint to
	 * @param renderer the series renderer
	 * @param titles the titles to go to the legend
	 * @param left the left X value of the area to draw to
	 * @param right the right X value of the area to draw to
	 * @param y the y value of the area to draw to
	 * @param width the width of the area to draw to
	 * @param height the height of the area to draw to
	 * @param legendSize the legend size
	 * @param paint the paint to be used for drawing
	 * @param calculate if only calculating the legend size
	 * 
	 * @return the legend height
	 */
	protected int pieDrawLegend(Canvas canvas, DefaultRenderer renderer, String[] titles, int left,
			int right, int y, int width, int height, int legendSize, int[][] pieCharColors, Paint paint,
			boolean calculate)
	{
		mPieChartColors = pieCharColors;
		return this.drawLegend(canvas, renderer, titles, left, right, y, width, height, legendSize, paint,
				calculate);
	}
	
	/**
	 * Draws the chart legend.
	 * 
	 * @param canvas the canvas to paint to
	 * @param renderer the series renderer
	 * @param titles the titles to go to the legend
	 * @param left the left X value of the area to draw to
	 * @param right the right X value of the area to draw to
	 * @param y the y value of the area to draw to
	 * @param width the width of the area to draw to
	 * @param height the height of the area to draw to
	 * @param legendSize the legend size
	 * @param paint the paint to be used for drawing
	 * @param calculate if only calculating the legend size
	 * 
	 * @return the legend height
	 */
	protected int drawLegend(Canvas canvas, DefaultRenderer renderer, String[] titles, int left, int right,
			int y, int width, int height, int legendSize, Paint paint, boolean calculate)
	{
		float size = 32;
		if (renderer.isShowLegend())
		{
			float currentX = left;
			float currentY = y + height - legendSize + size;
			paint.setTextAlign(Align.LEFT);
			paint.setTextSize(renderer.getLegendTextSize());
			int sLength = Math.min(titles.length, renderer.getSeriesRendererCount());
			for (int i = 0; i < sLength; i++)
			{
				final float lineSize = getLegendShapeWidth(i);
				String text = titles[i];
				if (titles.length == renderer.getSeriesRendererCount())
				{
					if (mPieChartColors != null)
					{
						paint.setColor(mPieChartColors[i][0]);
					}
					else
					{
						paint.setColor(renderer.getSeriesRendererAt(i).getColor());
					}
					
				}
				else
				{
					paint.setColor(Color.LTGRAY);
				}
				float[] widths = new float[text.length()];
				paint.getTextWidths(text, widths);
				float sum = 0;
				for (float value : widths)
				{
					sum += value;
				}
				float extraSize = lineSize + 10 + sum;
				float currentWidth = currentX + extraSize;
				
				if (i > 0 && getExceed(currentWidth, renderer, right, width))
				{
					currentX = left;
					currentY += renderer.getLegendTextSize();
					size += renderer.getLegendTextSize();
					currentWidth = currentX + extraSize;
				}
				if (getExceed(currentWidth, renderer, right, width))
				{
					float maxWidth = right - currentX - lineSize - 10;
					if (isVertical(renderer))
					{
						maxWidth = width - currentX - lineSize - 10;
					}
					int nr = paint.breakText(text, true, maxWidth, widths);
					text = text.substring(0, nr) + "...";
				}
				if (!calculate)
				{
					drawLegendShape(canvas, renderer.getSeriesRendererAt(i), currentX, currentY, i, paint);
					drawString(canvas, text, currentX + lineSize + 5, currentY + 5, paint);
				}
				currentX += extraSize;
			}
		}
		return Math.round(size + renderer.getLegendTextSize());
	}
	
	/**
	 * Draw a multiple lines string.
	 * 
	 * @param canvas the canvas to paint to
	 * @param text the text to be painted
	 * @param x the x value of the area to draw to
	 * @param y the y value of the area to draw to
	 * @param paint the paint to be used for drawing
	 */
	protected void drawString(Canvas canvas, String text, float x, float y, Paint paint)
	{
		String[] lines = text.split("\n");
		Rect rect = new Rect();
		int yOff = 0;
		for (int i = 0; i < lines.length; ++i)
		{
			canvas.drawText(lines[i], x, y + yOff, paint);
			paint.getTextBounds(lines[i], 0, lines[i].length(), rect);
			yOff = yOff + rect.height() + 5; // space between lines is 5
		}
	}
	
	/**
	 * Calculates if the current width exceeds the total width.
	 * 
	 * @param currentWidth the current width
	 * @param renderer the renderer
	 * @param right the right side pixel value
	 * @param width the total width
	 * @return if the current width exceeds the total width
	 */
	protected boolean getExceed(float currentWidth, DefaultRenderer renderer, int right, int width)
	{
		boolean exceed = currentWidth > right;
		if (isVertical(renderer))
		{
			exceed = currentWidth > width;
		}
		return exceed;
	}
	
	/**
	 * Checks if the current chart is rendered as vertical.
	 * 
	 * @param renderer the renderer
	 * @return if the chart is rendered as a vertical one
	 */
	public boolean isVertical(DefaultRenderer renderer)
	{
		return renderer instanceof XYMultipleSeriesRenderer
				&& ((XYMultipleSeriesRenderer) renderer).getOrientation() == Orientation.VERTICAL;
	}
	
	/**
	 * Makes sure the fraction digit is not displayed, if not needed.
	 * 
	 * @param label the input label value
	 * @return the label without the useless fraction digit
	 */
	protected String getLabel(double label)
	{
		String text = "";
		if (label == Math.round(label))
		{
			text = Math.round(label) + "";
		}
		else
		{
			text = label + "";
		}
		return text;
	}
	
	private static float[] calculateDrawPoints(float p1x, float p1y, float p2x, float p2y, int screenHeight,
			int screenWidth)
	{
		float drawP1x;
		float drawP1y;
		float drawP2x;
		float drawP2y;
		
		if (p1y > screenHeight)
		{
			// Intersection with the top of the screen
			float m = (p2y - p1y) / (p2x - p1x);
			drawP1x = (screenHeight - p1y + m * p1x) / m;
			drawP1y = screenHeight;
			
			if (drawP1x < 0)
			{
				// If Intersection is left of the screen we calculate the intersection
				// with the left border
				drawP1x = 0;
				drawP1y = p1y - m * p1x;
			}
			else if (drawP1x > screenWidth)
			{
				// If Intersection is right of the screen we calculate the intersection
				// with the right border
				drawP1x = screenWidth;
				drawP1y = m * screenWidth + p1y - m * p1x;
			}
		}
		else if (p1y < 0)
		{
			float m = (p2y - p1y) / (p2x - p1x);
			drawP1x = (-p1y + m * p1x) / m;
			drawP1y = 0;
			if (drawP1x < 0)
			{
				drawP1x = 0;
				drawP1y = p1y - m * p1x;
			}
			else if (drawP1x > screenWidth)
			{
				drawP1x = screenWidth;
				drawP1y = m * screenWidth + p1y - m * p1x;
			}
		}
		else
		{
			// If the point is in the screen use it
			drawP1x = p1x;
			drawP1y = p1y;
		}
		
		if (p2y > screenHeight)
		{
			float m = (p2y - p1y) / (p2x - p1x);
			drawP2x = (screenHeight - p1y + m * p1x) / m;
			drawP2y = screenHeight;
			if (drawP2x < 0)
			{
				drawP2x = 0;
				drawP2y = p1y - m * p1x;
			}
			else if (drawP2x > screenWidth)
			{
				drawP2x = screenWidth;
				drawP2y = m * screenWidth + p1y - m * p1x;
			}
		}
		else if (p2y < 0)
		{
			float m = (p2y - p1y) / (p2x - p1x);
			drawP2x = (-p1y + m * p1x) / m;
			drawP2y = 0;
			if (drawP2x < 0)
			{
				drawP2x = 0;
				drawP2y = p1y - m * p1x;
			}
			else if (drawP2x > screenWidth)
			{
				drawP2x = screenWidth;
				drawP2y = m * screenWidth + p1y - m * p1x;
			}
		}
		else
		{
			// If the point is in the screen use it
			drawP2x = p2x;
			drawP2y = p2y;
		}
		
		return new float[] { drawP1x, drawP1y, drawP2x, drawP2y };
	}
	
	/**
	 * The graphical representation of a path.
	 * 
	 * @param canvas the canvas to paint to
	 * @param points the points that are contained in the path to paint
	 * @param paint the paint to be used for painting
	 * @param circular if the path ends with the start point
	 */
	protected void drawPath(Canvas canvas, float[] points, Paint paint, boolean circular)
	{
		Path path = new Path();
		int height = canvas.getHeight();
		int width = canvas.getWidth();
		
		float[] tempDrawPoints;
		if (points.length < 4)
		{
			return;
		}
		tempDrawPoints = calculateDrawPoints(points[0], points[1], points[2], points[3], height, width);
		path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
		path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);
		
		for (int i = 4; i < points.length; i += 2)
		{
			if ((points[i - 1] < 0 && points[i + 1] < 0)
					|| (points[i - 1] > height && points[i + 1] > height))
			{
				continue;
			}
			tempDrawPoints = calculateDrawPoints(points[i - 2], points[i - 1], points[i], points[i + 1],
					height, width);
			if (!circular)
			{
				path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
			}
			path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);
		}
		if (circular)
		{
			path.lineTo(points[0], points[1]);
		}
		canvas.drawPath(path, paint);
	}
	
	/**
	 * Returns the legend shape width.
	 * 
	 * @param seriesIndex the series index
	 * @return the legend shape width
	 */
	public abstract int getLegendShapeWidth(int seriesIndex);
	
	/**
	 * The graphical representation of the legend shape.
	 * 
	 * @param canvas the canvas to paint to
	 * @param renderer the series renderer
	 * @param x the x value of the point the shape should be drawn at
	 * @param y the y value of the point the shape should be drawn at
	 * @param seriesIndex the series index
	 * @param paint the paint to be used for drawing
	 */
	public abstract void drawLegendShape(Canvas canvas, SimpleSeriesRenderer renderer, float x, float y,
			int seriesIndex, Paint paint);
	
	/**
	 * Calculates the best text to fit into the available space.
	 * 
	 * @param text the entire text
	 * @param width the width to fit the text into
	 * @param paint the paint
	 * @return the text to fit into the space
	 */
	private String getFitText(String text, float width, Paint paint)
	{
		String newText = text;
		int length = text.length();
		int diff = 0;
		while (paint.measureText(newText) > width && diff < length)
		{
			diff++;
			newText = text.substring(0, length - diff) + "...";
		}
		if (diff == length)
		{
			newText = "...";
		}
		return newText;
	}
	
	/**
	 * Calculates the current legend size.
	 * 
	 * @param renderer the renderer
	 * @param defaultHeight the default height
	 * @param extraHeight the added extra height
	 * @return the legend size
	 */
	protected int getLegendSize(DefaultRenderer renderer, int defaultHeight, float extraHeight)
	{
		int legendSize = renderer.getLegendHeight();
		if (renderer.isShowLegend() && legendSize == 0)
		{
			legendSize = defaultHeight;
		}
		if (!renderer.isShowLegend() && renderer.isShowLabels())
		{
			legendSize = (int) (renderer.getLabelsTextSize() * 4 / 3 + extraHeight);
		}
		return legendSize;
	}
	
	/**
	 * Draws a text label.
	 * 
	 * @param canvas the canvas
	 * @param labelText the label text
	 * @param renderer the renderer
	 * @param prevLabelsBounds the previous rendered label bounds
	 * @param centerX the round chart center on X axis
	 * @param centerY the round chart center on Y axis
	 * @param shortRadius the short radius for the round chart
	 * @param longRadius the long radius for the round chart
	 * @param currentAngle the current angle
	 * @param angle the label extra angle
	 * @param left the left side
	 * @param right the right side
	 * @param color the label color
	 * @param paint the paint
	 * @param line if a line to the label should be drawn
	 */
	protected void drawLabel(Canvas canvas, String labelText, DefaultRenderer renderer,
			List<RectF> prevLabelsBounds, int centerX, int centerY, float shortRadius, float longRadius,
			float currentAngle, float angle, int left, int right, int color, Paint paint, boolean line)
	{
		if (renderer.isShowLabels())
		{
			paint.setColor(color);
			double rAngle = Math.toRadians(90 - (currentAngle + angle / 2));
			double sinValue = Math.sin(rAngle);
			double cosValue = Math.cos(rAngle);
			int x1 = Math.round(centerX + (float) (shortRadius * sinValue));
			int y1 = Math.round(centerY + (float) (shortRadius * cosValue));
			int x2 = Math.round(centerX + (float) (longRadius * sinValue));
			int y2 = Math.round(centerY + (float) (longRadius * cosValue));
			
			float size = renderer.getLabelsTextSize();
			float extra = Math.max(size / 2, 10);
			paint.setTextAlign(Align.LEFT);
			if (x1 > x2)
			{
				extra = -extra;
				paint.setTextAlign(Align.RIGHT);
			}
			float xLabel = x2 + extra;
			float yLabel = y2;
			float width = right - xLabel;
			if (x1 > x2)
			{
				width = xLabel - left;
			}
			labelText = getFitText(labelText, width, paint);
			float widthLabel = paint.measureText(labelText);
			boolean okBounds = false;
			while (!okBounds && line)
			{
				boolean intersects = false;
				int length = prevLabelsBounds.size();
				for (int j = 0; j < length && !intersects; j++)
				{
					RectF prevLabelBounds = prevLabelsBounds.get(j);
					if (prevLabelBounds.intersects(xLabel, yLabel, xLabel + widthLabel, yLabel + size))
					{
						intersects = true;
						yLabel = Math.max(yLabel, prevLabelBounds.bottom);
					}
				}
				okBounds = !intersects;
			}
			
			if (line)
			{
				y2 = (int) (yLabel - size / 2);
				canvas.drawLine(x1, y1, x2, y2, paint);
				canvas.drawLine(x2, y2, x2 + extra, y2, paint);
			}
			else
			{
				paint.setTextAlign(Align.CENTER);
			}
			canvas.drawText(labelText, xLabel, yLabel, paint);
			if (line)
			{
				prevLabelsBounds.add(new RectF(xLabel, yLabel, xLabel + widthLabel, yLabel + size));
			}
		}
	}
	
	public boolean isNullValue(double value)
	{
		return Double.isNaN(value) || Double.isInfinite(value) || value == MathHelper.NULL_VALUE;
	}
	
	/**
	 * Given screen coordinates, returns the series and point indexes of a chart element. If there is no chart
	 * element (line, point, bar, etc) at those coordinates, null is returned.
	 * 
	 * @param screenPoint
	 * @return the series and point indexes
	 */
	public SeriesSelection getSeriesAndPointForScreenCoordinate(Point screenPoint)
	{
		return null;
	}
}
